import AuthenticationServices
import DictionaryCoding
import TutanotaSharedFramework
import UIKit
import UserNotifications
import WebKit

public enum InteropActions: String {
	case openSettings = "settings"
	case widget = "widget"
}

/// Main screen of the app.
class ViewController: UIViewController, WKNavigationDelegate, UIScrollViewDelegate {
	private let themeManager: ThemeManager
	private let alarmManager: AlarmManager
	private let notificationsHandler: NotificationsHandler
	private var bridge: RemoteBridge!
	private var webView: WKWebView!
	private var sqlCipherFacade: IosSqlCipherFacade

	private var keyboardSize = 0
	private var isDarkTheme = false

	init(
		crypto: TutanotaSharedFramework.IosNativeCryptoFacade,
		themeManager: ThemeManager,
		keychainManager: KeychainManager,
		notificationStorage: NotificationStorage,
		alarmManager: AlarmManager,
		notificaionsHandler: NotificationsHandler,
		credentialsEncryption: IosNativeCredentialsFacade,
		blobUtils: BlobUtil,
		contactsSynchronization: IosMobileContactsFacade,
		userPreferencesProvider: UserPreferencesProvider,
		urlSession: URLSession
	) {

		self.themeManager = themeManager
		self.alarmManager = alarmManager
		self.notificationsHandler = notificaionsHandler
		self.bridge = nil
		self.sqlCipherFacade = IosSqlCipherFacade()

		super.init(nibName: nil, bundle: nil)
		let webViewConfig = WKWebViewConfiguration()
		let folderPath: String = (self.appUrl() as NSURL).deletingLastPathComponent!.path
		webViewConfig.preferences.setValue(false, forKey: "allowFileAccessFromFileURLs")
		let apiSchemeHandler = ApiSchemeHandler(urlSession: urlSession)
		webViewConfig.setURLSchemeHandler(apiSchemeHandler, forURLScheme: "api")
		webViewConfig.setURLSchemeHandler(apiSchemeHandler, forURLScheme: "apis")
		webViewConfig.setURLSchemeHandler(AssetSchemeHandler(folderPath: folderPath), forURLScheme: "asset")

		self.webView = WKWebView(frame: CGRect.zero, configuration: webViewConfig)
		webView.navigationDelegate = self
		webView.scrollView.bounces = false
		webView.scrollView.isScrollEnabled = false
		webView.scrollView.delegate = self
		webView.isOpaque = false
		webView.scrollView.contentInsetAdjustmentBehavior = .never

		#if DEBUG
			if #available(iOS 16.4, *) { webView.isInspectable = true }
		#endif

		let commonSystemFacade = IosCommonSystemFacade(viewController: self, urlSession: urlSession)
		let userAgent = "\(self.webView.value(forKey: "userAgent") ?? "")"
		self.bridge = RemoteBridge(
			webView: self.webView,
			viewController: self,
			commonSystemFacade: commonSystemFacade,
			fileFacade: IosFileFacade(
				chooser: TUTFileChooser(viewController: self),
				viewer: FileViewer(viewController: self),
				schemeHandler: apiSchemeHandler,
				urlSession: urlSession
			),
			nativeCredentialsFacade: credentialsEncryption,
			nativeCryptoFacade: crypto,
			themeFacade: IosThemeFacade(themeManager: themeManager, viewController: self),
			appDelegate: self.appDelegate,
			alarmManager: self.alarmManager,
			notificationStorage: notificationStorage,
			keychainManager: keychainManager,
			webAuthnFacade: IosWebauthnFacade(viewController: self),
			sqlCipherFacade: self.sqlCipherFacade,
			contactsSynchronization: contactsSynchronization,
			userPreferencesProvider: userPreferencesProvider,
			externalCalendarFacade: ExternalCalendarFacadeImpl(urlSession: urlSession, userAgent: userAgent)
		)

	}

	required init?(coder: NSCoder) { fatalError("Not NSCodable") }

	override func loadView() {
		super.loadView()
		self.view.addSubview(webView)
		WebviewHacks.hideAccessoryBar()
		WebviewHacks.keyboardDisplayDoesNotRequireUserAction()

		NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
		NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
		NotificationCenter.default.addObserver(
			self,
			selector: #selector(onKeyboardSizeChange),
			name: UIResponder.keyboardDidChangeFrameNotification,
			object: nil
		)
	}

	/// Implementation of WKNavigationDelegate
	/// Handles links being clicked inside the webview
	func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
		guard let requestUrl = navigationAction.request.url else {
			decisionHandler(.cancel)
			return
		}

		if requestUrl.scheme == "asset" && requestUrl.path == self.getAssetUrl().path {
			decisionHandler(.allow)
		} else if requestUrl.scheme == "asset" && requestUrl.absoluteString.hasPrefix(self.getAssetUrl().absoluteString) {
			// If the app is removed from memory, the URL won't point to the file but will have additional path.
			// We ignore additional path for now.
			decisionHandler(.cancel)
			self.loadMainPage(params: [:])
		} else {
			decisionHandler(.cancel)
			UIApplication.shared.open(requestUrl, options: [:])
		}

	}

	var appDelegate: AppDelegate { get { UIApplication.shared.delegate as! AppDelegate } }

	func loadMainPage(params: [String: String]) { DispatchQueue.main.async { self._loadMainPage(params: params) } }

	@objc private func onKeyboardDidShow(note: Notification) {
		let rect = note.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
		self.onAnyKeyboardSizeChange(newHeight: rect.size.height)
	}

	@objc private func onKeyboardWillHide() { self.onAnyKeyboardSizeChange(newHeight: 0) }

	@objc private func onKeyboardSizeChange(note: Notification) {
		let rect = note.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
		let newHeight = rect.size.height
		if self.keyboardSize != 0 && self.keyboardSize != Int(newHeight) { self.onAnyKeyboardSizeChange(newHeight: newHeight) }
	}

	private func onAnyKeyboardSizeChange(newHeight: CGFloat) {
		self.keyboardSize = Int(newHeight)
		Task { try await MobileFacadeSendDispatcher(transport: self.bridge).keyboardSizeChanged(self.keyboardSize) }
	}

	func onApplicationDidEnterBackground() {
		// When the user leaves the app we want to perform "incremental_vacuum" on the offline database.
		// We perform vacuum once the app is put into background instead of when the app is terminated as on iOS
		// we do not have enough time before the app is terminated by the system.
		Task { try await self.sqlCipherFacade.vaccumDb() }
	}

	func onApplicationWillTerminate() { Task { try await self.sqlCipherFacade.closeDb() } }

	override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
		super.traitCollectionDidChange(previousTraitCollection)
		Task.detached { @MainActor in
			self.applyTheme(self.themeManager.currentThemeWithFallback)
			try? await self.bridge.commonNativeFacade.updateTheme()
		}
	}

	override func viewDidLoad() {
		super.viewDidLoad()
		webView.translatesAutoresizingMaskIntoConstraints = false
		webView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
		webView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
		webView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
		webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true

		let theme = self.themeManager.currentThemeWithFallback
		self.applyTheme(theme)
		self.notificationsHandler.initialize()

		Task { @MainActor in self._loadMainPage(params: [:]) }
	}

	private func _loadMainPage(params: [String: String]) {
		let fileUrl = self.getAssetUrl()

		var mutableParams = params
		if let theme = self.themeManager.currentTheme {
			let encodedTheme = self.dictToJson(dictionary: theme)
			mutableParams["theme"] = encodedTheme
		}
		mutableParams["platformId"] = "ios"
		let queryParams = NSURLQueryItem.from(dict: mutableParams)
		var components = URLComponents.init(url: fileUrl, resolvingAgainstBaseURL: false)!
		components.queryItems = queryParams

		let url = components.url!
		webView.load(URLRequest(url: url))
	}

	private func dictToJson(dictionary: [String: String]) -> String { try! String(data: JSONEncoder().encode(dictionary), encoding: .utf8) ?? "" }

	private func appUrl() -> URL {
		// this var is stored in Info.plist and possibly manipulated by the build schemes:
		// Product > Scheme > Manage Schemes in xcode.
		// default path points to the dist build of the web app,
		// both schemes modify it to point at the respective build before building the app
		let pagePath: String = Bundle.main.infoDictionary!["TutanotaApplicationPath"] as! String
		let path = Bundle.main.path(forResource: pagePath + "index-app", ofType: "html")
		if path == nil { return Bundle.main.resourceURL! } else { return NSURL.fileURL(withPath: path!) }
	}

	private func getAssetUrl() -> URL { URL(string: "asset://app/index-app.html")! }

	func applyTheme(_ theme: [String: String]) {
		let contentBgString = theme["surface"]!
		let contentBg = UIColor(hex: contentBgString)!
		self.isDarkTheme = !contentBg.isLight()
		self.view.backgroundColor = contentBg
		self.setNeedsStatusBarAppearanceUpdate()
	}

	func scrollViewDidScroll(_ scrollView: UIScrollView) {
		// disable scrolling of the web view to avoid that the keyboard moves the body out of the screen
		scrollView.contentOffset = CGPoint.zero
	}

	/// use the URL we were called with to retrieve the information about shared items
	/// from the app group storage
	private func getSharingInfo(url: URL) async -> SharingInfo? {
		guard let infoLocation = url.host else { return nil }
		return readSharingInfo(infoLocation: infoLocation)
	}

	func handleShare(_ url: URL) async throws {
		guard let info = await getSharingInfo(url: url) else {
			printLog("unable to get sharingInfo from url: \(url)")
			return
		}

		do { try await self.bridge.commonNativeFacade.createMailEditor(info.fileUrls.map { $0.path }, info.text, [], "", "") } catch {
			printLog("failed to open mail editor to share: \(error)")
			try FileUtils.deleteSharedStorage(subDir: info.identifier)
		}
	}

	private func getInteropAction(url: URL) async -> URLQueryItem? {
		guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil }

		return components.queryItems?.first
	}

	func handleInterop(_ url: URL) async throws {
		guard let interopAction = await getInteropAction(url: url) else {
			printLog("unable to get interop info from url: \(url)")
			return
		}

		do {
			if interopAction.name == InteropActions.openSettings.rawValue {
				try await self.bridge.commonNativeFacade.openSettings(interopAction.value!)
			} else if interopAction.name == InteropActions.widget.rawValue {
				try await handleWidgetActions(url, interopAction)
			}
		} catch { printLog("Failed to handle interop comunication for \(interopAction.name)=\(interopAction.value ?? ""): \(error)") }
	}

	func handleWidgetActions(_ url: URL, _ interopAction: URLQueryItem) async throws {
		guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { throw TutanotaError(message: "Invalid Widget Action URL") }
		guard let queryItems = components.queryItems else { throw TutanotaError(message: "Invalid Widget Action URL") }

		guard let userId = queryItems.first(where: { $0.name == "userId" })?.value else { throw TutanotaError(message: "Missing userId for Widget Action URL") }
		guard let date = queryItems.first(where: { $0.name == "date" })?.value else { throw TutanotaError(message: "Missing date for Widget Action URL") }
		let eventId = queryItems.first(where: { $0.name == "eventId" })?.value

		if interopAction.value == WidgetActions.sendLogs.rawValue { return try await sendLogsFromWidget() }

		let action = interopAction.value == WidgetActions.eventEditor.rawValue ? CalendarOpenAction.event_editor : CalendarOpenAction.agenda
		try await self.bridge.commonNativeFacade.openCalendar(userId, action, date, eventId)
	}

	func sendLogsFromWidget() async throws {
		let logs = readSharingInfo(infoLocation: WIDGET_LOGS_LOCATION)
		try await self.bridge.commonNativeFacade.sendLogs(logs?.text ?? "")
	}

	override var preferredStatusBarStyle: UIStatusBarStyle { if self.isDarkTheme { return .lightContent } else { return .darkContent } }
}

// Remove when webView config migration is removed
private class LittleNavigationDelegate: NSObject, WKNavigationDelegate {
	var action: (() -> Void)?

	func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { if let action = self.action { action() } }

	func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { TUTSLog("FAILED NAVIGATION >{") }
}

extension ViewController: ASWebAuthenticationPresentationContextProviding {
	func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { view.window! }
}
